UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan#54
UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan#54Quickkill0 wants to merge 58 commits into
Conversation
… recovery (#4) * feat: add multi-user auth, permissions, requests system - JWT authentication with BCrypt password hashing (access + refresh tokens) - User system with Admin/User roles and 13 granular permissions - Permission presets, invite link system, manga request workflow - Authorization middleware with dynamic policy provider - Bootstrap mode for initial setup when no users exist - SignalR hub JWT authentication - Login, register, setup pages and admin settings UI - Auth context provider with auto token refresh - API client interceptor for Bearer token management * feat: UI/UX overhaul — collapsible sidebar, mobile responsiveness, PWA support - Collapsible sidebar with pin/unpin and framer-motion animations - Frosted-glass header with breadcrumbs and centered search - Mobile: swipe-to-open sidebar, floating action button, pull-to-refresh - Responsive layouts across all pages (library, queue, settings, cloud-latest) - Provider-match dialog mobile responsiveness - PWA: service worker, offline fallback, install prompt, full manifest - Shimmer skeleton loading states - Download management UI and sidebar counters fix - Safe area insets for notched devices * feat: recover truncated manga titles and rename series files - Bridge-level title recovery: detect truncated titles (ending "..."/"…"), fetch detail page HTML, extract full title from og:title/twitter:title using LCP algorithm across multiple meta tag candidates - Automatic recovery during chapter checks and manual verify - NeedsRename flag + DB migration for tracking pending renames - Rename series files endpoint for updating storage paths after recovery - Update provider source titles when recovering truncated series title * build: rebuild Android.Compat.dll and add CI workflows - Rebuild Android.Compat.dll with updated dependencies - Replace Android.Compat.dll build workflow with LFS verification check - Update gradle build scripts and dependency versions * feat: provider health checks, download management, and backend improvements - Provider health check system with DB columns and migration - Download management controller and action enums - Improved provider cache service and dead provider handling - Startup hosted service improvements - Search query and command service refinements - Settings and image services updates - Various controller authorization and endpoint additions * chore: gitignore AI-generated artifacts * build: rebuild Android.Compat.dll Triggered by changes to AndroidCompat build configuration. Kotlin 2.3.0 * fix: sync provider titles when series title already recovered The verify recovery would skip updating provider titles when the series title had already been fixed in a prior deploy (srcLen == dbLen). Now provider titles are always synced against the best available title, regardless of whether the series title itself needed recovery. * fix: title recovery only triggers on truncated titles, respects IsTitle provider Two bugs fixed: 1. Recovery was comparing lengths only, treating any longer source title as a "recovery" — now requires the title to actually be truncated (ends with "..."/"…") before attempting recovery. 2. Was overwriting ALL provider titles with the recovered title — now only updates the IsTitle provider's title, never touches others. * fix: cache LFS objects in Docker CI to avoid bandwidth exhaustion Each build was downloading the 400MB Android.Compat.dll from LFS, burning through the 1GB/month free bandwidth in ~3 builds. Now uses actions/cache to persist LFS objects between runs. * fix: use GitHub Release for Android.Compat.dll instead of LFS in CI Docker build now downloads the DLL from the android-compat-v1 release instead of git lfs pull, avoiding LFS bandwidth limits entirely. The rebuild workflow auto-updates the release asset when the DLL changes. * fix: mobile drawers expand fully like add-series sheet Drawers using ResponsiveModal and RequestSeriesDialog were too short on mobile because they lacked flex-col layout and proper scroll containment. - Add flex flex-col to DrawerContent so flex-1 children fill height - Bump request-series drawer from 85dvh to 92dvh for consistency - Add overscroll-contain and data-vaul-no-drag for proper scroll behavior * `refactor: move max-h drawer constraint to DrawerContent base styles` - Set `h-[92dvh] flex flex-col` as default in `DrawerContent` component - Remove redundant `max-h-[92dvh] flex flex-col` classes from all `DrawerContent` usages across dialogs and drawers * chore: remove Git LFS in favor of CI-downloaded binaries - Remove LFS tracking for `.jar` and `.dll` files from `.gitattributes` - Add binary dependencies to `.gitignore` (downloaded by CI, not stored in repo) - Delete tracked LFS binary files (Android.Compat.dll, IKVM dlls, gradle-wrapper.jar) - Update docker-build workflow to skip LFS checkout - Update rebuild-androidcompat workflow to remove DLL commit step and LFS checkout - Switch IKVM references from local `libs/` DLLs to `PackageReference` (NuGet) - Use explicit Gradle version `9.2.1` via `gradle/actions/setup-gradle` instead of wrapper --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Implement per-title language detection for LatestSerieEntity entries - Add BackfillLatestSeriesLanguagesAsync to retag historical Browse-tab rows - Update SeriesExtensions to detect language from title for multi-language sources - Honor PreferredLanguages filter in SeriesQueryService even with specific sources - Include startup backfill with error handling and logging
- Add `genre` query parameter to `GetLatestAsync` endpoint - Implement client-side genre filtering with AND semantics - Add new `GetLatestGenresAsync` endpoint to retrieve distinct genres with counts - Add `IMemoryCache` dependency with 5-minute TTL for genre aggregation - Refactor `GetLatestAsync` to handle genre filtering with MaxGenreScanRows cap - Update method signatures and XML documentation comments
- Replace max-h with h and add flex layout to dialog content for proper scrolling - Add shrink-0 to dialog header to prevent it from shrinking - Update content wrapper to use flex-1 min-h-0 for proper flex child sizing - Add w-full max-w-full and overflow-hidden to series card for width constraints - Add break-words to description text for better word wrapping - Improve flex gap spacing in series card switches with gap-x-3 gap-y-2 - Add responsive shrink-0 to confirm step header on desktop - Make scrollable container flex-based on desktop with flex-1 min-h-0 - Add overflow-x-hidden to prevent horizontal scroll in series list - Update stepper container with flex-1 min-h-0 and add shrink-0 to stepper
- Move ActiveDownloadItem type and related components to queue-list-row - Extract sorting logic to queue-list-columns - Create QueueListView component for main list rendering - Remove unused imports (Card, Progress, Calendar, ExternalLink, etc.) - Reorganize imports and remove inline helper functions - Move constants to top-level section
SourceCell was missing `flex items-center` on its wrapper div, so the source label sat top-aligned while TimeCell and RetriesCell siblings were centered. Align it with the other cells across Active, Failed, and Recent row variants.
Removed outdated warning and migration notes for Kaizoku.NET 2.0.
Add a sticky bottom save button that appears when the header is hidden but save button is enabled, preventing duplicate save buttons and improving UX for settings forms that hide the header. Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
* feat(layout): command bar + activity dock + ribbon slot shell
Phase 1 of the command-bar UI redesign. Replaces the vertical sidebar +
horizontal header pair with a single sticky 56px command bar plus an
optional 48px contextual ribbon that pages fill via <RibbonSlot>.
New components under src/components/kzk/layout/:
- command-bar.tsx - sticky top bar with logo, section pills, search,
theme toggle, user menu, and the ribbon mount point.
Cmd+K focuses search; preserves swipe-from-left
gesture to open the mobile section drawer.
- section-pills.tsx - permission-gated horizontal nav with live counters
for Queue (active+failed) and Requests (pending);
pulses a dot on Queue when downloads are active.
Renders as a vertical list inside the mobile sheet.
- activity-dock.tsx - floating bottom-right panel that subscribes to the
same SignalR ProgressHub as the queue page (via
useDownloadProgress) and surfaces the current
download with progress, expandable to show up to
five. Suppressed on /queue and pre-login routes.
- ribbon.tsx - context-based slot system. Pages mount
<RibbonSlot>{controls}</RibbonSlot>; the bar
renders the most recently mounted slot's children.
Ribbon collapses to zero height when no slot is
mounted, so legacy pages keep working until they
migrate in phase 2.
- user-menu.tsx - UserAvatarDropdown extracted from the old header
so both desktop and mobile chrome share one source.
page-layout.tsx is rewritten to wrap children in RibbonProvider, render
the CommandBar, then the main content, then the ActivityDock.
The old sidebar.tsx, header.tsx, and breadcrumb.tsx remain on disk and
build cleanly but are no longer referenced by any active code path. They
get deleted in phase 4 after each page has migrated its controls into
the new ribbon.
Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors in series/page.tsx
and users-tab.tsx are unchanged).
* refactor(pages): hoist per-page controls into command-bar ribbon
Phase 2 of the command-bar UI redesign. Each page now renders its
contextual controls into the shared ribbon via <RibbonSlot>, instead of
inline below the page's own header.
The ribbon switched from a "context stores a ReactNode" pattern to a
React portal pattern. Pages render <RibbonSlot>{controls}</RibbonSlot>
which uses createPortal to render INTO a target div the command bar
publishes. This keeps the controls inside the page's React subtree so
context (Radix Tabs, search state, etc.) propagates correctly — storing
and re-rendering as a ReactNode in a different branch would have broken
Radix Tabs context for the Queue page.
Per-page changes:
* Library — the status / genre / source / sort / card-size selects and
the Add Series button move into the ribbon. PullToRefresh now wraps
only the grid.
* Queue — Tabs is now a controlled root that wraps both the RibbonSlot
(holding TabsList) and the page body (holding TabsContent). Radix
context flows from page → portal → ribbon.
* Settings — the tab strip moves into the ribbon as a pill group. The
page body keeps only the active tab's content panel.
* Browse / cloud-latest — source picker, tag popover trigger, and card
size move into the ribbon. The selected-genre chip strip stays inline
on the page (it's a removable-summary, not a control).
* Requests — page heading + pending count badge + history status filter
move into the ribbon. The History section keeps a plain heading that
echoes the active filter as a subtitle.
* Sources — only the page heading moves into the ribbon. The Installed /
Available filter chrome (search, NSFW toggle, language multi-select,
Check All) still lives inside ProviderManager because it's interleaved
with the two-column layout; lifting all four pieces of state up is a
follow-up refactor.
Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors (two pre-existing errors unchanged).
* feat(ui): cohere modals and series cards with command-bar glass language
Two polish passes that fold dialogs, drawers, sheets, and series cards
into the same visual language as the new command bar.
Modal primitives (dialog, sheet, drawer):
* Overlays switch from harsh black/80 to bg-background/70 with
backdrop-blur-sm + backdrop-saturate-150, so opening a modal feels
like a frosted veil over the page instead of a separate dark sheet.
* Content surfaces gain a soft hairline border (border-border/60),
a deeper shadow-2xl for legibility against the blurred overlay, and
a bumped corner radius (rounded-xl on dialogs, rounded-t-[12px] on
the mobile drawer) that matches the sidebar-replacement chrome.
* This cascades to every modal in the app — AddSeries wizard,
Setup wizard, Provider Match dialog, Deny / Confirm / Approve
dialogs, mobile section drawer — with no per-callsite edits.
Series cards (Library + Browse):
* New 2px status bar across the top edge, color-coded by
SeriesStatus via the existing getStatusDisplay() helper. Lets the
user spot Ongoing / Completed / On Hiatus / Disabled at a glance
without opening the card. Same treatment on Library cards
(list-series) and Browse cards (cloud-latest-grid) for a coherent
language between the two surfaces.
* Library cards: amber AlertTriangle dot just above the title strip
when series.hasUnknown is true — surfaces "needs a provider match"
on every card instead of burying it behind the Unassigned tab.
* Library cards: muted PauseCircle dot in the same position when
series.pausedDownloads is true (and not already showing the
unassigned dot).
The unread "+N" badge from the mockup is deferred — the SeriesInfo
API doesn't yet expose a read/unread chapter delta. Added once the
backend ships it.
Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.
* chore(layout): remove orphaned sidebar / header / breadcrumb
Phase 4 cleanup of the command-bar UI redesign. The sidebar, header,
and breadcrumb components have been replaced by command-bar.tsx +
section-pills.tsx + ribbon.tsx + activity-dock.tsx + user-menu.tsx
since phase 1, and nothing outside their own self-referential cluster
imports them. Deleting:
- src/components/kzk/layout/sidebar.tsx
- src/components/kzk/layout/header.tsx
- src/components/kzk/layout/breadcrumb.tsx
Also drops the now-unused --sidebar-collapsed-w / --sidebar-expanded-w
CSS variables from globals.css.
Build: ✓ next build succeeds (all 13 pages prerender).
Type-check: ✓ no new errors.
* fix(layout): break RibbonSlot infinite render loop (React #185)
The RibbonSlot effect previously depended on the full RibbonContext
value, which gets a new object identity every time the provider
re-renders. That produced an infinite loop on every page using a
ribbon:
1. RibbonSlot mounts → registerSlot(id) → setSlotIds adds id (new Set).
2. Provider re-renders → useMemo produces a new ctx value.
3. RibbonSlot's effect sees the new ctx → cleanup unregisters the id
(another new Set) and the body re-registers it (another new Set).
4. Two more state changes → provider re-renders → goto 2.
Fix: depend only on the stable registerSlot callback (already wrapped
in useCallback with [] deps) and the stable id from useId. The effect
now runs exactly once on mount and once on unmount.
Reproduces on every page load of the redesigned shell because every
page mounts at least one RibbonSlot. Production minified React
surfaces this as "Minified React error #185"; dev would show
"Maximum update depth exceeded".
Build: ✓ next build succeeds.
Type-check: ✓ no new errors.
* fix(ui): center nav pills, unclip browse tag popover, tidy queue tabs
- Command bar: drop Requests + Settings pills (still in user menu),
absolutely center the remaining section pills with a 60vw cap so they
can't overlap the logo or right cluster.
- Browse tag popover: portal to document.body with fixed coordinates
anchored to the trigger button so it isn't clipped by the ribbon's
overflow context; resize/scroll updates are coalesced via rAF.
- Queue page: center the TabsList in the ribbon and remove the redundant
active/queued/failed summary row — the counts are already on the tabs.
* fix(settings): center tab strip in command-bar ribbon
* feat(series): redesign detail page with Cinematic Hero layout
Replace the 2158-line monolithic series detail page with a layered glass
hero, restructured sources/downloads grid, and a series-scoped ribbon
mounted in the command bar.
- New SeriesHero: full-bleed blurred banner of the active cover with a
pink-tinted radial overlay, floating crisp cover, status pill with
pulse, title/author/meta/genres, read-more description, and an action
toolbar (Pause/Resume, Verify, Rename, Delete). Secondary actions go
icon-only under sm; storage path gains a copy-to-clipboard affordance.
- Extract ProviderCard, DownloadsPanel, DownloadItem, and getStatusIcon
into src/components/kzk/series/detail/. ProviderCard restyled with
pill-toggle Perm/Cover/Title controls, a 32x32 icon-button cluster,
and the project Dialog primitive for confirm-delete (was hand-rolled).
- New SourcesSection wraps the providers list with a header that owns
the Add Source button; AddSeries dialog is now controlled state.
- DownloadsPanel + DownloadItem restyled to compact rows with status
disc, indeterminate pink progress bar on RUNNING items, summary
footer with active/queued/failed counts, and View full queue link.
- New LibraryStatsCard on the right rail (chapters, active sources,
last update, storage health) replaces what would have been the
chapter list panel (deferred to follow-up).
- New SeriesRibbon mounted via RibbonSlot: Back to Library, centered
title (lg+), and an overflow menu mirroring the hero actions.
- 12-col grid with col-span-8 left (Sources) + col-span-4 right rail
(Downloads + Stats), sticky at top-[112px] (56 cmd bar + 48 ribbon
+ 8px gap) on lg+.
Behavior preserved verbatim: all 13 state vars, 8 mutations, 9 update
helpers, 7 useEffects (activity tracking, idle polling, cleanup on
unmount, context title sync, provider state init), and 5 dialogs
(Delete, Verify Success, Cleanup, ProviderMatch, AddSeries). Title and
cover exclusivity enforcement, idle 60s polling, and SignalR-driven
download refresh all unchanged.
Fixes a latent rename crash: setShowVerifySuccessDialog (undefined
setter) replaced with setShowVerifyDialog + synthetic verifyResult so
the success dialog actually renders after Rename Files completes.
* feat(series): per-chapter list panel with download + refresh
Adds a Chapters card to the series detail right rail so users can see
every chapter aggregated across providers, with download status, and
trigger downloads for missing chapters or force a refresh from sources.
Backend (.NET):
- GET /api/series/{id}/chapters returns List<ChapterDto> aggregated
across active providers, grouped by ChapterNumber (2-decimal round).
Status resolves to Downloaded > Queued > Missing across all providers
carrying the chapter; null-numbered chapters isolated by (providerId,
name) so unrelated specials don't collapse together. Providers[]
exposes which sources carry the chapter and which one(s) downloaded.
- POST /api/series/{id}/chapters/download takes optional chapterNumbers
body; omit to enqueue all missing chapters. Picks best provider per
chapter (active title provider first, then lowest ProviderIndex),
builds ChapterDownload via existing GenerateDownloadsFromChapterData,
feeds the existing QueueChapterDownloadsAsync pipeline, then
SaveChangesAsync to persist ShouldDownload mutations (the existing
pipeline doesn't save them — without this the Queued state never
surfaces).
- POST /api/series/{id}/chapters/refresh enqueues a high-priority
GetChapters job per active provider, reusing the canonical
JobBusinessService.BuildProviderGroupKey (extracted to a public
static helper so the inline group key used by the recurring
scheduler and the new on-demand path can never drift).
- Auth: CanViewLibrary on GET; CanEditSeries on the two POSTs.
Frontend:
- New types ChapterDto, ChapterProviderDto, ChapterDownloadStatus enum,
request/response DTOs in lib/api/types.ts.
- seriesService.getChaptersForSeries, triggerChapterDownloads (with
client-side 2-decimal rounding to match the backend filter),
refreshChapters.
- useChaptersForSeries hook polls every 10s while any chapter is
Queued (otherwise no polling). useDownloadMissingChapters and
useRefreshChapters invalidate the chapters cache (and downloads
cache for the first one) on success.
- New ChapterList component slotted into the right rail between
DownloadsPanel and LibraryStatsCard. Card header shows total +
missing count, a Refresh icon button (30s throttle, auto-clears
via setTimeout), and a Download All Missing button when there are
missing chapters. Filter chips (All/Downloaded/Missing) + sort
direction toggle. Per-row download button on missing chapters,
external link button on downloaded ones (both reveal on hover).
First 50 rows render by default with a Show all (N) expand.
Permissions are gated on canEditSeries throughout: Refresh, Download
All, and per-row Download all disappear for users without edit perms.
* fix(series): hero polish — layout, duplicate button, ribbon, button states
- Drop duplicate Add Source button. AddSeries was being mounted as a
controlled dialog in page.tsx, which also rendered its own internal
trigger as a stray floating pink button. SourcesSection now owns the
AddSeries instance directly via its triggerButton prop so there is
exactly one Add Source button (in the Sources card header) and one
modal.
- Strip the triple-dot overflow menu from SeriesRibbon. The hero
already exposes Pause/Verify/Rename/Delete; duplicating them in the
ribbon was redundant. Ribbon now is just Back to Library + centered
title. Nine props dropped.
- Rebalance the layout so the right rail doesn't tower over a 2-source
Sources card. Sources is now full-width on top with providers
tiling in a 2-col grid at lg+. Below: ChapterList in lg:col-span-8
with DownloadsPanel + LibraryStatsCard stacked in a lg:col-span-4
sticky aside. Chapters get the prominent placement they earn.
- Mobile hero action toolbar: Pause/Resume label now hidden under sm
(was always visible), matching the other actions. All icon margins
changed from mr-2 to sm:mr-2 so icons stay perfectly centered when
labels collapse. All four action buttons get px-0 w-9 sm:w-auto
sm:px-4 so they render as square icon-only buttons on mobile.
- Button feedback audit. Five buttons missing proper interactive
states now have hover/active/focus-visible feedback to project
standard:
* Storage path copy (hero) — adds active state, focus ring,
and a 1.5s "Copied" check-icon swap on click.
* Provider power toggle — green tint when enabled, gray when
disabled, active:scale-95 press feedback, focus ring.
* Chapter external-link anchor — active state + focus ring.
* Chapter sort toggle — full pill button styling (was bare
text), hover bg, active bg, focus ring.
* Download item row — focus-visible (was focus), active state,
inset focus ring for keyboard users.
* fix(series): revert Sources to single-column stack
The 2-col tile grid at lg+ crushed the title to ~150px (truncated to
"A Villai...") because ProviderCard's right-side controls cluster
(Perm/Cover/Title + After Ch. + power + delete) takes ~500px of fixed
width, leaving almost nothing for the title/metadata column.
Going back to a vertical stack at every breakpoint. Mobile was already
single-column and looked good — this preserves that and restores
desktop readability. Layout balance is still healthy because Chapters
took the prominent lg:col-span-8 slot in the previous commit.
---------
Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…va#10) * feat(layout): command bar + activity dock + ribbon slot shell Phase 1 of the command-bar UI redesign. Replaces the vertical sidebar + horizontal header pair with a single sticky 56px command bar plus an optional 48px contextual ribbon that pages fill via <RibbonSlot>. New components under src/components/kzk/layout/: - command-bar.tsx - sticky top bar with logo, section pills, search, theme toggle, user menu, and the ribbon mount point. Cmd+K focuses search; preserves swipe-from-left gesture to open the mobile section drawer. - section-pills.tsx - permission-gated horizontal nav with live counters for Queue (active+failed) and Requests (pending); pulses a dot on Queue when downloads are active. Renders as a vertical list inside the mobile sheet. - activity-dock.tsx - floating bottom-right panel that subscribes to the same SignalR ProgressHub as the queue page (via useDownloadProgress) and surfaces the current download with progress, expandable to show up to five. Suppressed on /queue and pre-login routes. - ribbon.tsx - context-based slot system. Pages mount <RibbonSlot>{controls}</RibbonSlot>; the bar renders the most recently mounted slot's children. Ribbon collapses to zero height when no slot is mounted, so legacy pages keep working until they migrate in phase 2. - user-menu.tsx - UserAvatarDropdown extracted from the old header so both desktop and mobile chrome share one source. page-layout.tsx is rewritten to wrap children in RibbonProvider, render the CommandBar, then the main content, then the ActivityDock. The old sidebar.tsx, header.tsx, and breadcrumb.tsx remain on disk and build cleanly but are no longer referenced by any active code path. They get deleted in phase 4 after each page has migrated its controls into the new ribbon. Build: ✓ next build succeeds (all 13 pages prerender). Type-check: ✓ no new errors (two pre-existing errors in series/page.tsx and users-tab.tsx are unchanged). * refactor(pages): hoist per-page controls into command-bar ribbon Phase 2 of the command-bar UI redesign. Each page now renders its contextual controls into the shared ribbon via <RibbonSlot>, instead of inline below the page's own header. The ribbon switched from a "context stores a ReactNode" pattern to a React portal pattern. Pages render <RibbonSlot>{controls}</RibbonSlot> which uses createPortal to render INTO a target div the command bar publishes. This keeps the controls inside the page's React subtree so context (Radix Tabs, search state, etc.) propagates correctly — storing and re-rendering as a ReactNode in a different branch would have broken Radix Tabs context for the Queue page. Per-page changes: * Library — the status / genre / source / sort / card-size selects and the Add Series button move into the ribbon. PullToRefresh now wraps only the grid. * Queue — Tabs is now a controlled root that wraps both the RibbonSlot (holding TabsList) and the page body (holding TabsContent). Radix context flows from page → portal → ribbon. * Settings — the tab strip moves into the ribbon as a pill group. The page body keeps only the active tab's content panel. * Browse / cloud-latest — source picker, tag popover trigger, and card size move into the ribbon. The selected-genre chip strip stays inline on the page (it's a removable-summary, not a control). * Requests — page heading + pending count badge + history status filter move into the ribbon. The History section keeps a plain heading that echoes the active filter as a subtitle. * Sources — only the page heading moves into the ribbon. The Installed / Available filter chrome (search, NSFW toggle, language multi-select, Check All) still lives inside ProviderManager because it's interleaved with the two-column layout; lifting all four pieces of state up is a follow-up refactor. Build: ✓ next build succeeds (all 13 pages prerender). Type-check: ✓ no new errors (two pre-existing errors unchanged). * feat(ui): cohere modals and series cards with command-bar glass language Two polish passes that fold dialogs, drawers, sheets, and series cards into the same visual language as the new command bar. Modal primitives (dialog, sheet, drawer): * Overlays switch from harsh black/80 to bg-background/70 with backdrop-blur-sm + backdrop-saturate-150, so opening a modal feels like a frosted veil over the page instead of a separate dark sheet. * Content surfaces gain a soft hairline border (border-border/60), a deeper shadow-2xl for legibility against the blurred overlay, and a bumped corner radius (rounded-xl on dialogs, rounded-t-[12px] on the mobile drawer) that matches the sidebar-replacement chrome. * This cascades to every modal in the app — AddSeries wizard, Setup wizard, Provider Match dialog, Deny / Confirm / Approve dialogs, mobile section drawer — with no per-callsite edits. Series cards (Library + Browse): * New 2px status bar across the top edge, color-coded by SeriesStatus via the existing getStatusDisplay() helper. Lets the user spot Ongoing / Completed / On Hiatus / Disabled at a glance without opening the card. Same treatment on Library cards (list-series) and Browse cards (cloud-latest-grid) for a coherent language between the two surfaces. * Library cards: amber AlertTriangle dot just above the title strip when series.hasUnknown is true — surfaces "needs a provider match" on every card instead of burying it behind the Unassigned tab. * Library cards: muted PauseCircle dot in the same position when series.pausedDownloads is true (and not already showing the unassigned dot). The unread "+N" badge from the mockup is deferred — the SeriesInfo API doesn't yet expose a read/unread chapter delta. Added once the backend ships it. Build: ✓ next build succeeds (all 13 pages prerender). Type-check: ✓ no new errors. * chore(layout): remove orphaned sidebar / header / breadcrumb Phase 4 cleanup of the command-bar UI redesign. The sidebar, header, and breadcrumb components have been replaced by command-bar.tsx + section-pills.tsx + ribbon.tsx + activity-dock.tsx + user-menu.tsx since phase 1, and nothing outside their own self-referential cluster imports them. Deleting: - src/components/kzk/layout/sidebar.tsx - src/components/kzk/layout/header.tsx - src/components/kzk/layout/breadcrumb.tsx Also drops the now-unused --sidebar-collapsed-w / --sidebar-expanded-w CSS variables from globals.css. Build: ✓ next build succeeds (all 13 pages prerender). Type-check: ✓ no new errors. * fix(layout): break RibbonSlot infinite render loop (React #185) The RibbonSlot effect previously depended on the full RibbonContext value, which gets a new object identity every time the provider re-renders. That produced an infinite loop on every page using a ribbon: 1. RibbonSlot mounts → registerSlot(id) → setSlotIds adds id (new Set). 2. Provider re-renders → useMemo produces a new ctx value. 3. RibbonSlot's effect sees the new ctx → cleanup unregisters the id (another new Set) and the body re-registers it (another new Set). 4. Two more state changes → provider re-renders → goto 2. Fix: depend only on the stable registerSlot callback (already wrapped in useCallback with [] deps) and the stable id from useId. The effect now runs exactly once on mount and once on unmount. Reproduces on every page load of the redesigned shell because every page mounts at least one RibbonSlot. Production minified React surfaces this as "Minified React error #185"; dev would show "Maximum update depth exceeded". Build: ✓ next build succeeds. Type-check: ✓ no new errors. * fix(ui): center nav pills, unclip browse tag popover, tidy queue tabs - Command bar: drop Requests + Settings pills (still in user menu), absolutely center the remaining section pills with a 60vw cap so they can't overlap the logo or right cluster. - Browse tag popover: portal to document.body with fixed coordinates anchored to the trigger button so it isn't clipped by the ribbon's overflow context; resize/scroll updates are coalesced via rAF. - Queue page: center the TabsList in the ribbon and remove the redundant active/queued/failed summary row — the counts are already on the tabs. * fix(settings): center tab strip in command-bar ribbon * feat(series): redesign detail page with Cinematic Hero layout Replace the 2158-line monolithic series detail page with a layered glass hero, restructured sources/downloads grid, and a series-scoped ribbon mounted in the command bar. - New SeriesHero: full-bleed blurred banner of the active cover with a pink-tinted radial overlay, floating crisp cover, status pill with pulse, title/author/meta/genres, read-more description, and an action toolbar (Pause/Resume, Verify, Rename, Delete). Secondary actions go icon-only under sm; storage path gains a copy-to-clipboard affordance. - Extract ProviderCard, DownloadsPanel, DownloadItem, and getStatusIcon into src/components/kzk/series/detail/. ProviderCard restyled with pill-toggle Perm/Cover/Title controls, a 32x32 icon-button cluster, and the project Dialog primitive for confirm-delete (was hand-rolled). - New SourcesSection wraps the providers list with a header that owns the Add Source button; AddSeries dialog is now controlled state. - DownloadsPanel + DownloadItem restyled to compact rows with status disc, indeterminate pink progress bar on RUNNING items, summary footer with active/queued/failed counts, and View full queue link. - New LibraryStatsCard on the right rail (chapters, active sources, last update, storage health) replaces what would have been the chapter list panel (deferred to follow-up). - New SeriesRibbon mounted via RibbonSlot: Back to Library, centered title (lg+), and an overflow menu mirroring the hero actions. - 12-col grid with col-span-8 left (Sources) + col-span-4 right rail (Downloads + Stats), sticky at top-[112px] (56 cmd bar + 48 ribbon + 8px gap) on lg+. Behavior preserved verbatim: all 13 state vars, 8 mutations, 9 update helpers, 7 useEffects (activity tracking, idle polling, cleanup on unmount, context title sync, provider state init), and 5 dialogs (Delete, Verify Success, Cleanup, ProviderMatch, AddSeries). Title and cover exclusivity enforcement, idle 60s polling, and SignalR-driven download refresh all unchanged. Fixes a latent rename crash: setShowVerifySuccessDialog (undefined setter) replaced with setShowVerifyDialog + synthetic verifyResult so the success dialog actually renders after Rename Files completes. * feat(series): per-chapter list panel with download + refresh Adds a Chapters card to the series detail right rail so users can see every chapter aggregated across providers, with download status, and trigger downloads for missing chapters or force a refresh from sources. Backend (.NET): - GET /api/series/{id}/chapters returns List<ChapterDto> aggregated across active providers, grouped by ChapterNumber (2-decimal round). Status resolves to Downloaded > Queued > Missing across all providers carrying the chapter; null-numbered chapters isolated by (providerId, name) so unrelated specials don't collapse together. Providers[] exposes which sources carry the chapter and which one(s) downloaded. - POST /api/series/{id}/chapters/download takes optional chapterNumbers body; omit to enqueue all missing chapters. Picks best provider per chapter (active title provider first, then lowest ProviderIndex), builds ChapterDownload via existing GenerateDownloadsFromChapterData, feeds the existing QueueChapterDownloadsAsync pipeline, then SaveChangesAsync to persist ShouldDownload mutations (the existing pipeline doesn't save them — without this the Queued state never surfaces). - POST /api/series/{id}/chapters/refresh enqueues a high-priority GetChapters job per active provider, reusing the canonical JobBusinessService.BuildProviderGroupKey (extracted to a public static helper so the inline group key used by the recurring scheduler and the new on-demand path can never drift). - Auth: CanViewLibrary on GET; CanEditSeries on the two POSTs. Frontend: - New types ChapterDto, ChapterProviderDto, ChapterDownloadStatus enum, request/response DTOs in lib/api/types.ts. - seriesService.getChaptersForSeries, triggerChapterDownloads (with client-side 2-decimal rounding to match the backend filter), refreshChapters. - useChaptersForSeries hook polls every 10s while any chapter is Queued (otherwise no polling). useDownloadMissingChapters and useRefreshChapters invalidate the chapters cache (and downloads cache for the first one) on success. - New ChapterList component slotted into the right rail between DownloadsPanel and LibraryStatsCard. Card header shows total + missing count, a Refresh icon button (30s throttle, auto-clears via setTimeout), and a Download All Missing button when there are missing chapters. Filter chips (All/Downloaded/Missing) + sort direction toggle. Per-row download button on missing chapters, external link button on downloaded ones (both reveal on hover). First 50 rows render by default with a Show all (N) expand. Permissions are gated on canEditSeries throughout: Refresh, Download All, and per-row Download all disappear for users without edit perms. * fix(series): hero polish — layout, duplicate button, ribbon, button states - Drop duplicate Add Source button. AddSeries was being mounted as a controlled dialog in page.tsx, which also rendered its own internal trigger as a stray floating pink button. SourcesSection now owns the AddSeries instance directly via its triggerButton prop so there is exactly one Add Source button (in the Sources card header) and one modal. - Strip the triple-dot overflow menu from SeriesRibbon. The hero already exposes Pause/Verify/Rename/Delete; duplicating them in the ribbon was redundant. Ribbon now is just Back to Library + centered title. Nine props dropped. - Rebalance the layout so the right rail doesn't tower over a 2-source Sources card. Sources is now full-width on top with providers tiling in a 2-col grid at lg+. Below: ChapterList in lg:col-span-8 with DownloadsPanel + LibraryStatsCard stacked in a lg:col-span-4 sticky aside. Chapters get the prominent placement they earn. - Mobile hero action toolbar: Pause/Resume label now hidden under sm (was always visible), matching the other actions. All icon margins changed from mr-2 to sm:mr-2 so icons stay perfectly centered when labels collapse. All four action buttons get px-0 w-9 sm:w-auto sm:px-4 so they render as square icon-only buttons on mobile. - Button feedback audit. Five buttons missing proper interactive states now have hover/active/focus-visible feedback to project standard: * Storage path copy (hero) — adds active state, focus ring, and a 1.5s "Copied" check-icon swap on click. * Provider power toggle — green tint when enabled, gray when disabled, active:scale-95 press feedback, focus ring. * Chapter external-link anchor — active state + focus ring. * Chapter sort toggle — full pill button styling (was bare text), hover bg, active bg, focus ring. * Download item row — focus-visible (was focus), active state, inset focus ring for keyboard users. * fix(series): revert Sources to single-column stack The 2-col tile grid at lg+ crushed the title to ~150px (truncated to "A Villai...") because ProviderCard's right-side controls cluster (Perm/Cover/Title + After Ch. + power + delete) takes ~500px of fixed width, leaving almost nothing for the title/metadata column. Going back to a vertical stack at every breakpoint. Mobile was already single-column and looked good — this preserves that and restores desktop readability. Layout balance is still healthy because Chapters took the prominent lg:col-span-8 slot in the previous commit. --------- Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…a#11) * feat(library/browse): redesign with cinematic SpotlightHero Library and Browse pages now lead with a 420px cinematic hero that extends the language introduced on series-detail to the two top-level catalog surfaces. Single shared component, two variants. New: src/components/kzk/series/spotlight-hero.tsx - Full-bleed blurred backdrop of the active series' cover, pink radial top-right overlay, dark legibility veil. - Floating crisp cover (200x280) with the 2px status-color strip that already appears on every card. - Eyebrow + display title (<h2>), status pill with pulse for ONGOING, meta strip (variant-aware), gradient-faded description, tag chips, single pink primary CTA. - Right-side dot strip cycles up to 5 items; auto-advances every 8s and pauses for 12s after a manual dot click. Crossfades the backdrop + cover + content together (350ms). - Honors prefers-reduced-motion (no auto-advance, no fade on manual click). Pauses entirely when document.hidden, resumes on visibilitychange. - Tab/tabpanel a11y wired via useId: each dot has role=tab, aria-controls and a stable id; panel has role=tabpanel, aria-labelledby points at the active tab. Backdrop image is aria-hidden. - Timer lifecycle: separate refs for the outer schedule timer and the inner crossfade timer, both cleared on cleanup / items change / visibility hidden so the component never calls setState on an unmounted node. - Returns null when items.length === 0. Library page (src/app/library/page.tsx): - Spotlight pool = top 5 series by lastChangeUTC desc, falling back to most-recently-added when timestamps are null. - CTA "Open series" routes to /library/series?id=... - Eyebrow: "SPOTLIGHT - From your library" - Session-storage helpers hoisted to module scope. - Duplicate-series detection logs gated behind NODE_ENV != production. Browse page (src/app/cloud-latest/page.tsx): - Spotlight pool = page-0 snapshot filtered to InLibraryStatus.NotInLibrary, sorted by fetchDate desc, first 5. Snapshot taken from the first fetch only so the hero never churns during infinite scroll. - CTA "Add to library" opens the existing AddSeries dialog pre-populated with the spotlight item's title (uses the AddSeries component's existing controlled-open API; no new prop required). - Eyebrow: "DISCOVER - From your sources" - Session-storage helpers hoisted to module scope. - Idle-refresh console logs gated behind NODE_ENV != production. The card grid, ribbon controls (status dropdown, genre, sources, sort, card size, Add Series), search context wiring, infinite scroll, and the existing AddSeries / ProviderMatch flows are preserved verbatim. Build: next build PASS (all 13 routes prerender). Type-check: tsc --noEmit PASS (one pre-existing users-tab.tsx error unchanged, unrelated to this diff). * chore: add library/browse redesign mockups Three interactive HTML mockups created during the design exploration phase for the library + browse redesign. Each is a single self- contained file (TailwindCSS via CDN, vanilla JS, no build) showing a different design direction over the recently-shipped command-bar + ribbon shell. - library-browse-A-curated-shelf.html (editorial / row-based) - library-browse-B-cinematic-galaxy.html (hero-led / immersive) - library-browse-C-dense-power-grid.html (info-rich / power-user) Mockup B is the direction implemented in the previous commit. These are reference artifacts only - not used by the React app and not imported anywhere. Useful for future design iterations and for PR reviewers comparing alternatives. * fix(browse): randomize spotlight pool instead of sorting by fetchDate The Browse spotlight previously surfaced the 5 most-recently-fetched items from the active source, but those same items are already the top of the grid directly below the hero — duplicating them in the spotlight wastes the prime real estate. Browse is a discovery surface, so the hero is more useful when it pulls something the user wouldn't have scrolled past anyway. Replaces the fetchDate sort with a Fisher-Yates shuffle of the NotInLibrary subset of the page-0 snapshot, then takes 5. Re-shuffles only when filters refresh the first page (source / genre / search), so the hero stays stable during infinite scroll. Build: next build PASS. Type-check: tsc --noEmit PASS (one pre-existing users-tab.tsx error unchanged). * fix(spotlight): add hover, press, and focus-visible states to CTA The spotlight CTA button looked static because the inline boxShadow won the cascade over any Tailwind hover modifier — inline styles beat class rules, so the original markup couldn't express any interactive feedback. Moves the resting shadow into a className so layered states can override it cleanly. Adds: - hover: brightness-110 + a stronger, larger pink glow - active: scale 0.97 + brightness-95 + a softer, tighter shadow (matches the project standard from the series-detail polish pass) - focus-visible: 2px ring in lighter pink with a background- colored offset for keyboard users - transition-all 150ms ease-out - motion-reduce:transition-none so reduced-motion users still get instant state changes without animation Affects both the Library "Open series" CTA and the Browse "Add to library" CTA, which share the same Button render in SpotlightHero. --------- Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Update cloud-latest and library pages to fetch 7 items instead of 5 - Increase SpotlightHero component capacity to display up to 7 items - Add expandable description with "Read more/less" toggle - Collapse description when switching between spotlight items - Improve mobile layout with centered text and justified content - Add proper ARIA attributes for accessibility
…axpiva#12) * refactor: replace ProviderManager with SourcesList component - Import SourcesList instead of ProviderManager in providers page - Remove ProviderManager-specific props (isCompact, showSearch, grid columns, titles) - Remove outdated comment about sources contextual ribbon - Extract utility functions to sources/lib module - Add comprehensive sources page styling with CSS custom properties and component classes * refactor: move search input to global header, simplify toolbar - Remove `setSearchTerm` prop from SourcesList and SourcesToolbar components - Move search functionality to global command-bar (header), aligning with Library/Browse pattern - Replace MultiSelect language filter with DropdownMenu + checkbox list - Add "Select All" toggle for language filtering - Remove mobile search sheet and related focus logic - Add JSDoc comments for SourcesList props - Update imports to reflect removed/added components --------- Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
…axpiva#13) * refactor: restructure queue page with simplified filtering and grouping - Replace tab-based layout with filter pill segmented control - Simplify row components to unified QueueRow with callbacks - Remove sorting functionality and complex column logic - Add date-based grouping (today, yesterday, this-week, earlier) - Implement virtualization with MAX_VISIBLE limit for performance - Consolidate dialog and state management logic - Move utility functions to dedicated utils module * feat: add download progress display to queue items - Add progress field to QueueRowItem interface (0-100%) - Display progress percentage instead of "downloading" label - Add animated progress bar at bottom of downloading rows - Use tabular-nums font for percentage display consistency --------- Co-authored-by: Quickkill0 <Quickkill0@users.noreply.github.com>
- Add `Unreadable` enum value to `ArchiveResult` for files that cannot be read - Introduce `TryGetArchiveFileNames()` method that reports read success/failure - Update `IsArchive()` to return `Unreadable` instead of `NotAnArchive` for transient failures - Add `ReconcileOrphanedFilesAsync()` to recover chapters from incorrectly blanked DB rows - Improve documentation to clarify handling of locked files and I/O errors
- Add `System.Globalization` and `System.Text.RegularExpressions` imports - Implement `KaizokuFilenameRegex` to parse Kaizoku filename pattern `[provider(-scanlator)?][lang]? title chapter (chapterName?)` - Add `ParsedFileKey` struct to hold parsed filename components (provider, scanlator, language, chapter) - Add `TryParseKaizokuFilename()` method to extract metadata from archive filenames - Change file collection from `HashSet<string>` to `List<string>` for sequential parsing - Add disk basename validation set to distinguish existing links from orphaned files - Implement batch parsing of disk files with fallback for legacy archives - Update reconciliation logic to match files by parsed metadata instead of filename patterns - Improve robustness against series title changes, padding shifts, and chapter renames
- Add `retries` field to QueueRowItem interface - Pass retry count from queue page to queue row items - Display retry count badge (×N) next to chapter label when retries > 0
Adds .github/workflows/guard-binaries.yml to fail PRs and pushes that
introduce build outputs or files over 50 MB (10 MB for .jar). Forbidden
paths cover the known build outputs (Android.Compat.dll, IKVM CILPatcher
libs, gradle build/libs/*.jar, .NET bin/{Release,Debug}/*.dll).
Adds .githooks/pre-commit with the same checks for fast local feedback.
Opt in per clone with: git config core.hooksPath .githooks
Forces LF line endings for hooks and workflow YAML so they execute on
Linux CI runners regardless of developer git autocrlf settings.
Build artifacts are published via the 'rebuild-androidcompat' workflow
to GitHub Releases; this guard prevents accidental re-introduction.
The previous 'branches-ignore: [!refs/tags/*]' clause used invalid syntax and excluded all branches. Switch to 'branches: [**]' which matches any branch push and naturally excludes tag pushes.
The previous 'Update release asset' step called 'gh release upload' unconditionally, which fails on a fork where the 'android-compat-v1' release has never been created. Switch to a create-or-update flow that detects whether the release exists and either creates it (fresh forks) or updates it (subsequent rebuilds).
Cherry-picked from upstream maxpiva/Rensaio@cccbb1d (Fixes: maxpiva#48 maxpiva#42). Handles JSON array, bracket-comma, and plain string formats when parsing provider preference StringCollection values. The other changes in cccbb1d are intentionally skipped: - StartupHostedService.cs (regression — comments out PRAGMA busy_timeout) - Internal.Tests project addition (not needed yet) - Frontend page changes (conflict with our UI redesign) - tmpclaude-6db8-cwd (stray temp file)
- Reframe as community-maintained build of maxpiva/Kaizoku.NET - Add explicit lineage line (OAE -> maxpiva -> this build) - Remove outdated demo video and brightness comment - Switch Docker examples to ghcr.io/quickkill0/kaizoku.net:main - Update Unraid template URLs and icon to this repo - Update Desktop App releases link to this repo - Drop self-deprecating commentary and unfinished-state jokes - Normalize 'Mihon' casing (was MIHON) - Tighten tone overall while keeping emojis as section markers
Builds KaizokuTray Avalonia desktop app as self-contained single-file binaries for win-x64, win-arm64, linux-x64, linux-arm64. Cross-compiles all targets from ubuntu-latest (no Windows runner needed since builds are unsigned). Triggers: - Push of a v* tag (auto) - Manual workflow_dispatch with version input The release job is idempotent: creates the release on first run, or updates assets on re-runs using the same pattern as rebuild-androidcompat.
KaizokuTray transitively references Mihon.ExtensionsBridge.Core which depends on Android.Compat.dll at compile time. Without it, the build fails with CS0246 errors for androidx/eu/okhttp3/kotlin namespaces. Mirrors the pattern already used by docker-build.yml: pull the prebuilt DLL from the android-compat-v1 release before dotnet publish.
The tray transitively references KaizokuBackend which embeds wwwroot.zip
and wwwroot.sha256 as resources. These are generated from the frontend
build, not committed.
Restructure into three jobs:
prepare -> downloads Android.Compat.dll, builds frontend, packages
wwwroot.zip + sha256, uploads as artifact
build (matrix) -> downloads prep artifact, builds tray for each RID
release -> publishes assets to GitHub Release
Frontend now builds once instead of 4x in parallel.
- Add webkit appearance-none styles to search inputs to hide browser default cancel button and search decoration - Import useEffect and useRef hooks in search context - Implement section-based search term clearing to prevent search state pollution across main tabs (Library, Browse, Queue, Sources) - Add sectionKey memoization to track current section based on pathname - Clear search input when user switches between main sections while preserving restored state on page load
…ring - Remove Docker-specific path handling in storage path resolution - Redesign series ribbon title to use absolute centering for proper alignment with command bar - Add clarifying comments for layout behavior on different screen sizes - Delete obsolete mockup file
- Import and configure Fraunces and JetBrains Mono fonts from Google Fonts - Apply font variables to html root element - Unify desktop/mobile UI by replacing Drawer/Dialog conditional with single Dialog component - Simplify add-series modal with custom styling for responsive behavior - Remove useMediaQuery dependency from add-series components - Replace Card/Tooltip with Collapsible for series details UI
- Update dialog content className with improved responsive sizing and positioning - Add close button (X icon) to dialog header with proper accessibility - Remove inline padding styles, use CSS classes instead - Relocate source filter pills below search input for better UX - Adjust image sizes and error message styling - Remove redundant mobile footer component
- Move source palette and hashing logic to dedicated component - Replace pill-based source filter with MultiSelectSources dropdown - Update dialog width from 820px to 980px for better layout - Simplify search status indicator with loading spinner - Remove decorative swatch colors from result badges - Remove unused metadata elements from series results
- Add wrapper divs for better layout control in confirm and search steps - Group CTA buttons into dedicated container for mobile responsiveness - Implement scrollable containers with proper flex sizing - Update CSS for sources row alignment and result list padding - Adjust mobile breakpoint styles for compact button layout - Add clearance padding to accommodate sticky CTAs on mobile
- Simplify grid from 4 columns (checkbox/source/lang/chapters) to 3 columns (checkbox/source+flag/chapters) - Move language flag inline with source name instead of separate column - Update grid-template-columns to use `minmax(0, 1fr)` for proper overflow handling - Add `.ch-head`, `.flag`, `.scanlator`, and `.sep` CSS classes for better styling organization - Add `truncate` class to scanlator text to prevent overflow - Update `.name-row` and `.sub-row` CSS with flexbox layout and proper spacing - Remove unused `.swatch` divs and inline styles in favor of CSS classes
- Display mini cover thumbnails (32x48px) in source table rows
- Add fourth column to source table grid for thumbnail images
- Adjust grid layout and padding to accommodate new column
- Shorten mobile label text ("Permanent Source" → "Permanent", etc.)
- Add styling for `.src-cv-mini` with gradient background and shadow effect
- Add optional `shortLabel` prop to `PillToggle` component - Display short labels on mobile and full labels on desktop using Tailwind responsive classes - Update provider toggle labels with more descriptive full text: - "Perm" → "Permanent Source" - "Cover" → "Use as Cover" - "Title" → "Use as Title"
- Create reusable WizardShell component for wizard UI layout - Simplify step definitions using new WizardStep interface - Remove redundant UI component imports and icon usage - Consolidate error display logic - Extract navigation handlers into separate functions
• Remove unused Check icon import from lucide-react • Replace Check component with ✓ unicode character in series selection indicator • Simplify "Apply Match" button text by removing icon wrapper • Add ProviderRow component for displaying read-only provider snapshots • Improve thumbnail fallback logic to include provider snapshots • Add ExternalLink icon and country flag support to import card
- Add ThumbCacheService dependency to SetupWizardController - Populate thumb cache keys with public API URLs in GetImportsAsync - Back-fill cover URLs from live SeriesProviderEntity into filesystem snapshots - Fix ResizeObserver to use borderBoxSize for accurate row height measurement - Update card wrapper padding comments for clarity
- Remove ThumbCacheService dependency from SetupWizardController - Simplify GetImportsAsync endpoint to return raw import data - Remove provider thumbnail back-fill logic from ImportCommandService - Remove ProviderRow component and related thumbnail display code
…e 1)
Align the fork's auth schema with maxpiva/Kaizoku.NET's planned design while
preserving fork enhancements (granular permissions, presets, sessions table).
- Add UserLevel {User,Manager,Admin} enum alongside existing UserRole; backfill
Level from Role (Admin->Admin, User->User)
- Add OpdsPath (unique) with OpdsPathGenerator + startup backfill
- Move avatars from disk (AvatarPath) to DB blob (AvatarBlob + AvatarContentType)
- Add PasswordSetToken; make PasswordHash/Salt nullable (no-password users)
- Drop Email from login/register/DTOs (username-only login); legacy columns
retained nullable for transition
- Converge schema creation: EF model + ReconcileUserSchema migration + idempotent
PRAGMA-guarded startup upgrade path; unique OpdsPath index created only after
backfill to avoid constraint violations on existing multi-user installs
Make authentication optional and disabled-by-default with a no-password profile-selection mode, per upstream multi-user plan. Existing JWT login remains the path used when auth is enabled. - Add AuthenticationEnabled + ExternalDomain to EditableSettingsDto (persisted as SettingEntity rows; default disabled) - AuthSettingsCache: fail-closed in-memory cache (returns auth-required until positively loaded) + eager warm-up in Program before serving requests, so an auth-enabled instance enforces auth from the very first request - AuthMiddleware: dual-mode user resolution into HttpContext.Items["User"] — X-Kaizoku-User header (disabled) vs JWT principal hydration (enabled); exact allow-list matching for public endpoints - RequireUserLevelAttribute: no-ops when auth disabled; enforces UserLevel via UserService.ResolveLevel otherwise - BootstrapModeMiddleware reduced to first-user detection (no longer blocks API) - AuthController: status now reports authenticationEnabled/hasUsers + user list in disabled mode; add POST /api/auth/select-user (no-password sign-in)
Two startup blockers surfaced when building/running Phases 1-2: - Fresh install crashed with "no such table: Settings": the eager auth-cache warm-up in Program.cs opened an AppDbContext against a missing SQLite file, creating an empty kaizoku.db that fooled MigrationService (File.Exists check) into the legacy v1.0 path and skipping schema creation. Guard the warm-up with File.Exists so it is skipped on brand-new installs (fail-closed cache keeps auth required). - DI validation (ValidateOnBuild/ValidateScopes in Development) threw at host.Build() due to captive scoped-in-singleton dependencies. Resolve NouisanceFixer20ExtraLarge from the per-run scope in StartupHostedService instead of constructor-injecting it, and register IRepositoryDownloader as a singleton (it is stateless) instead of scoped.
…mode
Controllers authorize via the claims principal (RequireAdmin /
RequirePermission policies and User.FindFirst("UserId")). In disabled
mode no principal existed, so every [Authorize] endpoint returned 401
and request attribution was impossible.
AuthMiddleware now builds an authenticated ClaimsPrincipal in disabled
mode from the X-Kaizoku-User profile (or the primary active admin when
no/invalid header), reusing the same claim set as JWT issuance. This
makes the existing authorization stack work identically in both modes
with zero controller changes, and enforces the 12 granular permissions
per profile even without passwords. Enabled mode is untouched — the
header is ignored there and a JWT is still required.
- AuthService: extract JWT claim list into static BuildUserClaims so
JWT and disabled-mode principals share one source of truth.
- AuthMiddleware: build principal + set context.User in disabled mode;
default-admin fallback; zero-user installs stay anonymous so setup
endpoints remain reachable.
The homepage fired hundreds of unbounded concurrent /api/image requests; each opened a scoped SQLite connection, EF blocked thread-pool threads on serialized SQLite, and the pool spawned OS threads until the kernel refused pthread_create (surfaced as OutOfMemoryException from Thread.StartInternal). Backend: - Bound the image endpoint with a configurable SemaphoreSlim gate (MaxImageConcurrency, default 12); volatile-published, zero-clamped. - Switch AppDbContext to AddDbContextPool (poolSize 64). - Stop holding _urlLock/_eTagLock across DB I/O in ThumbCacheService (GetKeyAsync, GetEtagAsync, PopulateThumbsAsync). - Serialize check-then-insert per-URL to prevent duplicate cache rows. - Chunk Url IN-clause existence queries to <=500 (SQLite 999-var limit). - Add OutputCache (300s) on cover responses to bypass the DB path. - entrypoint.sh: ulimit -s 1024 / -u 4096 as a native-thread backstop. Frontend: - Drop blanket Image priority on covers; lazy-load all but the first row and correct the sizes hints to the real tile width.
Adds the no-password user model and invite/set-password flow so an admin
can create profiles without passwords (disabled/profile-picker mode) and
hand out one-time links for users to set their own password.
- UserEntity: add PasswordSetTokenExpiresAt (nullable) via the runtime
schema-reconcile path (EnsureAuthTablesAsync CREATE TABLE + idempotent
AddColumnIfMissing), mirroring the PasswordSetToken precedent.
- UserService.CreateAsync: create a no-password user when password is blank;
add GenerateInviteTokenAsync (URL-safe 32-byte token, 7-day expiry),
SetPasswordWithTokenAsync (validates token+expiry+policy, sets hash,
clears token, revokes sessions), ClaimUserAsync, AnyAdminHasPasswordAsync.
Invite/set-password/claim require the target user to be active.
- UserInviteService: builds a shareable set-password URL from ExternalDomain
(only when it is an absolute http(s) URL, else relative).
- Endpoints: POST /api/auth/set-password (anon), set-admin-password (admin),
POST /api/users/first (anon, passwordless-capable), PUT /api/users/{id}/claim
(anon, disabled-mode only), POST /api/users/{id}/generate-invite (admin).
- Lockout guard: refuse to enable authentication (409 needsPassword) unless an
active admin has a password, fired only on the false->true transition.
- claim is no longer allow-listed in enabled mode (requires JWT) and is
rejected by the controller when auth is enabled — dual-layer defense.
- SetupGate: single process-wide lock shared by /auth/setup and /users/first
to close the first-user check-then-create race; /auth/setup stays
password-required and atomic.
- AuthStatusDto.NeedsAdminPassword surfaced for the frontend (queried only in
disabled mode with users present).
- Dual-mode API client: Bearer token when auth enabled, X-Kaizoku-User header in profile-picker mode; ApiError carries status + body - New /user-select profile picker and /set-password invite-token pages - Auth context: select-user flow, createFirstUser, refreshStatus, needsAdminPassword; logout routes to /login or /user-select by mode - Setup/register/login pages: email removed, optional password (passwordless profiles), username-only login - Settings users tab: AuthenticationSection toggle with 409 needs-password recovery dialog, invite-link generation, hasPassword badge, OPDS path display - Avatars render from avatarBase64 data URIs (plain img, next/image does not support data URIs)
When Authentication:ResetFirstAdminPassword is true at startup, the first active admin's password and pending set-password token are cleared, their refresh sessions revoked, and password authentication disabled (falling back to profile-picker mode) so the admin can sign in and set a new password from Settings. The flag is rewritten to false in the data-folder appsettings.json so the reset is one-shot. Also uses the previously dead IConfiguration ctor param and documents the flag in the appsettings.json template (add-only merge preserves user-set values).
Backend:
- AuthMiddleware: only guard /api and /progress surfaces; honor
[AllowAnonymous] endpoint metadata so the SPA, static assets and
anonymous endpoints work with authentication enabled (H1)
- Rebuild legacy Users table (NOT NULL Email/PasswordHash/Salt from
main) via SQLite 12-step rebuild with foreign_keys=OFF on a pinned
connection, preserving child rows (H2)
- D.8 reset: also flip the in-memory EditableSettingsDto so a later
settings save cannot re-enable authentication from stale state (H3)
- ReconcileUserSchema migration body removed (no-op); the guarded
EnsureAuthTablesAsync owns auth schema evolution (M1)
- select-user now verifies a claimed profile's password and answers
401 { passwordRequired: true }; status exposes hasPassword (M2)
- Generic login error for passwordless users to prevent username
enumeration (L1)
- Invite/password-set tokens stored SHA-256-hashed at rest (L2)
- CreateUserDto.permissionPresetId applied as permission base;
explicit permissions still override (contract)
- PUT profile returns full UserDetailDto; preferences update is
partial (all fields optional) instead of clobbering (contract)
Frontend:
- Clear stale JWTs in profile-picker mode so a leftover Bearer token
cannot shadow X-Kaizoku-User and resolve the wrong user (H1)
- queryClient.clear() on every identity change (login, logout,
register, profile select) so cached data never leaks across
profiles (H2)
- Password prompt on the profile picker for claimed profiles, with
lock badges and 401-driven retry (M2)
- 401 token refresh only attempted when an access token exists, so
picker-mode 401s surface as ApiError instead of forced logout
- Setup page no longer bounces to /user-select before the created
admin is loaded (M3)
- Auth settings save: post-save status-refresh/logout failures are no
longer mislabeled as save failures, and toggling authentication no
longer silently persists an unsaved external-domain edit (L4)
Multi-user alignment: dual-mode auth (profile picker + optional JWT)
Resolved conflicts by keeping fork implementations and porting upstream's fixes into them: - StartupHostedService: comment out busy_timeout pragma per upstream - library/page, provider-match-dialog, confirm-series-step: filter empty values out of Radix Select item lists - provider-preferences-requester: adopt EMPTY_VALUE_SENTINEL handling so Radix Select never receives empty-string item values Excluded upstream's accidental temp file KaizokuFrontend/tmpclaude-6db8-cwd. https://claude.ai/code/session_011wVxZhooprqT2kiC1x25Yu
The settings tab strip (and queue filter pills) used w-full justify-center inside the ribbon's overflow-x-auto container. When the centered content overflowed, the left half extended past the scroll origin and could not be scrolled into view, hiding the General tab on narrow screens. Use mx-auto on the inner element instead: auto margins center it when it fits and collapse to zero on overflow, keeping every tab reachable. https://claude.ai/code/session_01GGp1u953QjgXwv2QbGXLfi
|
Hold on, please. I haven’t committed it yet, but I already implemented OPDS, the OPDS server, source and provider status page, users, user management, scrobbling, external scrobbling clients, and several QOL improvements. I’m currently fixing bugs and will commit RC1 now so you can get a taste of it. Could you wait one more week? What’s missing is listed below. Please don’t tackle any of this yet; I’ll fix it myself. Just give me one week. Fix external client scrobbling. Right now, MAL and Kitsu are partially broken. AniList has its own auth broken, and the others have not been fully tested yet. The goal is to make everything flawless and auto-sync read states. Current permission logic: Owners and Admins can manage settings and users. Other roles cannot. Managers and above can manage series and source providers, including adding editing pause/unpause them, but they cannot delete series, source providers, or sources. They also cannot acknowledge Series and Provider Status. Only Owners and Admins can delete series providers, source providers, or sources. They can also pause/unpause and acknowledge Series and Provider Status. Users only have read-only access to series, series providers, and sources. They cannot manage them. Trackers/scrobblers and self-user-management functionality are open to all roles. This includes managing, connecting, disconnecting, matching, and similar actions. Backend approach: enforce the permission logic and return Forbidden when the user does not have the required rights. Frontend approach: hide anything the user does not have permission to use. Pages and sidebar options should be hidden from users who do not have access to them. Buttons and calls to action should also be hidden when the user does not have the required permissions. Settings is still missing external domain / reverse proxy setup. |
|
Ill hold off on changing anything else until your ready. When youve done what you want, let me know and i will consolidate the changes. The main offload for this is a substantial ui redesign, as my user structure still diverges from yours in certain places (user permissions and such). |
Add a Screenshots section showcasing the library, series detail, sources, queue, search, and add-series views. Images were resized from ultrawide captures (3438px wide) to 1600px and the cover-art heavy library shots re-encoded as JPEG to keep the repo light.
Leaked OkHttp Response objects were never returned to the connection pool, slowly exhausting native memory and handles until the CLR could no longer allocate a thread stack (OutOfMemoryException at thread start after long uptime). - SourceInterop.DownloadUrlAsync / GetPageImageAsync: close response before throwing on non-200 status. - SourceInterop.TryRecoverFullTitleAsync: wrap response in a closing guard (was never closed). - ContentTypeStream.ReadAllBytes: move close calls into finally so a mid-stream read error cannot skip them. - Add ResponseCloser disposable helper.
UI overhaul + multi-user implementation aligned with the 2.0RC.1 plan
This PR upstreams the work done on the Quickkill0 fork: a broad UI/UX refresh across the app and a multi-user/auth implementation deliberately reconciled with the multi-user plan on the
2.0RC.1branch so it can land without diverging from your roadmap. It is based on currentmain(includescccbb1d, the #48/#42 fixes, merged with conflicts resolved in favor of those fixes).Multi-user & auth (aligned with the 2.0RC.1 plan)
ResetFirstAdminPasswordflag to recover the first admin account.UI
Backend fixes & infrastructure
StringCollectiondeserialization for provider preferences.Android.Compat.dllremoved from the repo (downloaded at build time).Notes for review
cccbb1dkeeps your empty-string Select fixes (EMPTY_VALUE_SENTINEL, empty-value filters) and the disabledbusy_timeoutpragma, ported into the redesigned components.KaizokuFrontend/tmpclaude-6db8-cwdis removed in this PR.